Skip to content

fix(core): preserve list item type when pasting into empty list items#2722

Open
nperez0111 wants to merge 3 commits intomainfrom
feat/fix-issue-2330
Open

fix(core): preserve list item type when pasting into empty list items#2722
nperez0111 wants to merge 3 commits intomainfrom
feat/fix-issue-2330

Conversation

@nperez0111
Copy link
Copy Markdown
Contributor

@nperez0111 nperez0111 commented May 7, 2026

Summary

Fixes #2330. Pasting plain text (or a paragraph) into an empty bullet/numbered/check list item used to replace the list item with a paragraph; it now keeps its type and absorbs the pasted inline content. Also fixes bare <li>a</li><li>b</li> HTML pastes that previously produced a list item followed by a paragraph instead of two list items.

Rationale

BlockNote's editor.pasteHTML round-trips through the BlockNote HTML serializer, producing a closed slice (blockGroup > blockContainer > paragraph) that ProseMirror inserts as a new block — walking up the document and replacing the surrounding container. That's surprising when the user is just pasting text into an empty list item. Separately, the BulletListItem parse rule requires a <ul>/<ol> parent, so orphan <li> HTML fell back to paragraph parsing.

Changes

  • packages/core/src/editor/transformPasted.tsretypeLeadingParagraphForEmptyTarget retypes the slice's leading paragraph to match an empty, non-paragraph, inline-content target block (list items, headings, custom inline-content blocks). Subsequent blocks are kept as-is and become siblings. Non-paragraph leading blocks (heading, list item) keep the existing replace behavior.
  • packages/core/src/api/parsers/html/util/nestedLists.ts — new wrapOrphanListItems step wraps consecutive <li> siblings with no <ul>/<ol> ancestor in a fresh <ul> before list-lifting runs.

Impact

Scoped: only fires when the target is an empty inline-content block whose type isn't paragraph and the slice's leading block is a paragraph. Pasting into non-empty blocks, into paragraphs, or pasting non-paragraph leading blocks (heading, list item, table) is unchanged. The orphan-<li> wrap only affects HTML where <li> has no list ancestor.

Testing

  • New packages/core/src/editor/transformPasted.test.ts (15 tests) drives the actual paste path via editor.pasteHTML for: paragraph into empty bullet/numbered/check list items, paragraphs with marks, multi-paragraph paste, heading paste, heading + paragraph paste, list item paste, nested list paste, bare <li> paste, two list items into empty/non-empty list items, and regression cases for non-empty list items and paragraphs.
  • New snapshot tests in packages/core/src/api/parsers/html/util/nestedLists.test.ts cover orphan <li> wrapping and confirm <li>s already inside <ul> are left alone.
  • All existing core (441) and tests-package (844) tests pass.

Checklist

  • Code follows the project's coding standards.
  • Unit tests covering the new feature have been added.
  • All existing tests pass.
  • The documentation has been updated to reflect the new feature

Summary by CodeRabbit

  • New Features

    • Orphaned
    • elements in pasted/parsed HTML are automatically grouped and wrapped into proper list containers to preserve list structure.
  • Bug Fixes

    • Paste handling improved: leading paragraphs in pasted content can be retyped to match empty inline-target blocks, preserving inline formatting and document structure.
  • Tests

    • Added comprehensive tests covering orphan list wrapping, nested list preservation, and many paste scenarios for lists, headings, and paragraphs.

…tent blocks

Fixes #2330

Pasting plain text or a paragraph into an empty bullet/numbered/check list
item replaced the list item with a paragraph because BlockNote's serializer
wraps content in `blockGroup > blockContainer > paragraph`, producing a
closed slice that ProseMirror inserts as a new block rather than splicing
inline. `transformPasted` now retypes the leading paragraph in such a slice
to match the empty target block, so the list item keeps its type and any
trailing blocks become siblings.

Also fixes bare `<li>a</li><li>b</li>` HTML parsing: the BulletListItem
parse rule requires a `<ul>`/`<ol>` parent, so orphan `<li>`s used to fall
back to paragraphs. `nestedListsToBlockNoteStructure` now wraps consecutive
orphan `<li>` siblings in a fresh `<ul>` before parsing.
@vercel
Copy link
Copy Markdown

vercel Bot commented May 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
blocknote Ready Ready Preview May 7, 2026 0:16am
blocknote-website Ready Ready Preview May 7, 2026 0:16am

Request Review

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 7, 2026

Review Change Stack
No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a67740fb-fb9e-4528-b2e7-36637795a6df

📥 Commits

Reviewing files that changed from the base of the PR and between 49fe92c and 38743b4.

⛔ Files ignored due to path filters (1)
  • packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
📒 Files selected for processing (3)
  • packages/core/src/api/parsers/html/util/nestedLists.test.ts
  • packages/core/src/api/parsers/html/util/nestedLists.ts
  • packages/core/src/editor/transformPasted.ts

📝 Walkthrough

Walkthrough

Preprocesses HTML to wrap top-level orphan

  • elements into
      groups and adds paste-time retyping so pasting into empty inline-content blocks preserves the target block type; test suites for both behaviors are added or expanded.

      Changes

      List Item Paste Handling Improvements

      Layer / File(s) Summary
      Orphan List Items Preprocessing
      packages/core/src/api/parsers/html/util/nestedLists.ts
      Introduces wrapOrphanListItems(element) to group bare top-level <li> siblings into new <ul> elements and calls it from nestedListsToBlockNoteStructure before existing list lifting steps.
      Orphan Wrapping Tests
      packages/core/src/api/parsers/html/util/nestedLists.test.ts
      Adds tests for consecutive orphan <li>, single orphan, orphans mixed with other top-level nodes, existing <ul>-contained <li> unchanged, and nested orphan <li> conversion to inner lists.
      Paste Retyping Logic
      packages/core/src/editor/transformPasted.ts
      Adds retypeLeadingParagraphForEmptyTarget() and invokes it from transformPasted to replace a pasted leading paragraph with the target block type when inserting into an empty non-paragraph inline-content block.
      Comprehensive Paste Tests
      packages/core/src/editor/transformPasted.test.ts
      Adds tests covering many paste scenarios (paragraph, heading, list-item, bare <li>, nested lists) into empty and non-empty targets, inline-mark preservation, multi-paragraph handling, and a paragraph-regression check.

      Estimated code review effort

      🎯 3 (Moderate) | ⏱️ ~25 minutes

      Possibly related PRs

      • TypeCellOS/BlockNote#2551: Touches the same HTML preprocessing pipeline around nestedListsToBlockNoteStructure; both PRs add preprocessing steps in that area.

      Poem

      🐰 I hop through HTML, tidy each stray line,
      Bare

    • find homes in new lists I design,
      Paste comes along, I keep block types true,
      First para stays yours, the rest splits through,
      Tiny fixes, tidy hops — a rabbit's review!

  • 🚥 Pre-merge checks | ✅ 4 | ❌ 1

    ❌ Failed checks (1 warning)

    Check name Status Explanation Resolution
    Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
    ✅ Passed checks (4 passed)
    Check name Status Explanation
    Title check ✅ Passed The title 'fix(core): preserve list item type when pasting into empty list items' clearly describes the main change—fixing list item type preservation during paste operations on empty items.
    Description check ✅ Passed The PR description comprehensively covers all template sections: Summary, Rationale, Changes (with file details), Impact, and Testing; only the documentation update (non-critical) is noted as incomplete.
    Linked Issues check ✅ Passed The PR changes directly address #2330 by preserving list item type when pasting text/paragraphs into empty list items and fixing orphan <li> HTML parsing via wrapOrphanListItems.
    Out of Scope Changes check ✅ Passed All changes are scoped to the linked objectives: retypeLeadingParagraphForEmptyTarget handles empty inline-content targets, wrapOrphanListItems fixes orphan <li> wrapping, and tests verify these scenarios without unrelated modifications.

    ✏️ Tip: You can configure your own custom pre-merge checks in the settings.

    ✨ Finishing Touches
    📝 Generate docstrings
    • Create stacked PR
    • Commit on current branch
    🧪 Generate unit tests (beta)
    • Create PR with unit tests
    • Commit unit tests in branch feat/fix-issue-2330

    Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

    ❤️ Share

    Comment @coderabbitai help to get the list of available commands and usage tips.

    Copy link
    Copy Markdown

    @coderabbitai coderabbitai Bot left a comment

    Choose a reason for hiding this comment

    The reason will be displayed to describe this comment to others. Learn more.

    Actionable comments posted: 2

    🧹 Nitpick comments (1)
    packages/core/src/api/parsers/html/util/nestedLists.test.ts (1)

    146-169: ⚡ Quick win

    Add a regression test for non-whitespace separators between orphan <li> nodes.

    Current cases miss <li>a</li>text<li>b</li>, which should not be merged into one wrapped list.

    🤖 Prompt for AI Agents
    Verify each finding against current code. Fix only still-valid issues, skip the
    rest with a brief reason, keep changes minimal, and validate.
    
    In `@packages/core/src/api/parsers/html/util/nestedLists.test.ts` around lines 146
    - 169, Add a regression test in nestedLists.test.ts that ensures non-whitespace
    separators prevent orphan <li> nodes from being merged: create an it(...) (e.g.,
    "Does not merge <li> nodes separated by non-whitespace text") that calls
    testHTML with the HTML string `<li>a</li>text<li>b</li>` and asserts the output
    does NOT wrap both <li> elements into one <ul>; use the existing test pattern
    (the testHTML helper) and mirror surrounding test style so the new case
    integrates with other cases like "Wraps consecutive bare <li> elements in a
    <ul>" and "Wraps bare <li>s mixed with other top-level content".
    
    🤖 Prompt for all review comments with AI agents
    Verify each finding against current code. Fix only still-valid issues, skip the
    rest with a brief reason, keep changes minimal, and validate.
    
    Inline comments:
    In `@packages/core/src/api/parsers/html/util/nestedLists.ts`:
    - Around line 34-38: The grouping loop uses nextElementSibling which skips text
    nodes and therefore merges <li> across meaningful text; change the iteration to
    use nextSibling (start from orphan.nextSibling) and in the while loop stop if
    you encounter a text node with non-whitespace content (nodeType ===
    Node.TEXT_NODE && node.textContent.trim() !== '') or any node that is not an LI
    element; only treat nodes as continued list items when the node is an Element
    with tagName "LI" and orphanSet.has(node as HTMLElement). Update variables
    referenced (next, orphan, orphanSet, group, handled) accordingly so grouping
    only proceeds across whitespace text nodes but not across meaningful text.
    
    In `@packages/core/src/editor/transformPasted.ts`:
    - Around line 198-242: The helper retypeLeadingParagraphForEmptyTarget is called
    unconditionally from transformPasted using
    getBlockInfoFromSelection(view.state), which is unsafe for drop operations whose
    insertion target may differ from the selection; update transformPasted to only
    call retypeLeadingParagraphForEmptyTarget for paste flows (check transaction
    metadata similar to shouldApplyFix) or add a target-equivalence check before
    invoking retypeLeadingParagraphForEmptyTarget (compare drop insertion point
    target vs selection-derived target), and add a regression test covering drops
    into empty list-item targets to ensure correctness.
    
    ---
    
    Nitpick comments:
    In `@packages/core/src/api/parsers/html/util/nestedLists.test.ts`:
    - Around line 146-169: Add a regression test in nestedLists.test.ts that ensures
    non-whitespace separators prevent orphan <li> nodes from being merged: create an
    it(...) (e.g., "Does not merge <li> nodes separated by non-whitespace text")
    that calls testHTML with the HTML string `<li>a</li>text<li>b</li>` and asserts
    the output does NOT wrap both <li> elements into one <ul>; use the existing test
    pattern (the testHTML helper) and mirror surrounding test style so the new case
    integrates with other cases like "Wraps consecutive bare <li> elements in a
    <ul>" and "Wraps bare <li>s mixed with other top-level content".
    
    🪄 Autofix (Beta)

    Fix all unresolved CodeRabbit comments on this PR:

    • Push a commit to this branch (recommended)
    • Create a new PR with the fixes

    ℹ️ Review info
    ⚙️ Run configuration

    Configuration used: Organization UI

    Review profile: CHILL

    Plan: Pro

    Run ID: 421fd044-3488-4c9a-9d23-9a5f085b1b7e

    📥 Commits

    Reviewing files that changed from the base of the PR and between 1b53232 and 73e7a02.

    ⛔ Files ignored due to path filters (1)
    • packages/core/src/api/parsers/html/util/__snapshots__/nestedLists.test.ts.snap is excluded by !**/*.snap, !**/__snapshots__/**
    📒 Files selected for processing (4)
    • packages/core/src/api/parsers/html/util/nestedLists.test.ts
    • packages/core/src/api/parsers/html/util/nestedLists.ts
    • packages/core/src/editor/transformPasted.test.ts
    • packages/core/src/editor/transformPasted.ts

    Comment thread packages/core/src/api/parsers/html/util/nestedLists.ts Outdated
    Comment thread packages/core/src/editor/transformPasted.ts
    @pkg-pr-new
    Copy link
    Copy Markdown

    pkg-pr-new Bot commented May 7, 2026

    Open in StackBlitz

    @blocknote/ariakit

    npm i https://pkg.pr.new/@blocknote/ariakit@2722
    

    @blocknote/code-block

    npm i https://pkg.pr.new/@blocknote/code-block@2722
    

    @blocknote/core

    npm i https://pkg.pr.new/@blocknote/core@2722
    

    @blocknote/mantine

    npm i https://pkg.pr.new/@blocknote/mantine@2722
    

    @blocknote/react

    npm i https://pkg.pr.new/@blocknote/react@2722
    

    @blocknote/server-util

    npm i https://pkg.pr.new/@blocknote/server-util@2722
    

    @blocknote/shadcn

    npm i https://pkg.pr.new/@blocknote/shadcn@2722
    

    @blocknote/xl-ai

    npm i https://pkg.pr.new/@blocknote/xl-ai@2722
    

    @blocknote/xl-docx-exporter

    npm i https://pkg.pr.new/@blocknote/xl-docx-exporter@2722
    

    @blocknote/xl-email-exporter

    npm i https://pkg.pr.new/@blocknote/xl-email-exporter@2722
    

    @blocknote/xl-multi-column

    npm i https://pkg.pr.new/@blocknote/xl-multi-column@2722
    

    @blocknote/xl-odt-exporter

    npm i https://pkg.pr.new/@blocknote/xl-odt-exporter@2722
    

    @blocknote/xl-pdf-exporter

    npm i https://pkg.pr.new/@blocknote/xl-pdf-exporter@2722
    

    commit: 38743b4

    - nestedLists: walk siblings via nextSibling so meaningful (non-whitespace)
      text between bare <li>s prevents them from being merged into one <ul>.
      Whitespace text nodes still bridge consecutive orphans.
    - transformPasted: bail out of retypeLeadingParagraphForEmptyTarget during
      drop events (view.dragging is set), since the slice is inserted at the
      drop point rather than the current selection.
    Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

    Labels

    None yet

    Projects

    None yet

    Development

    Successfully merging this pull request may close these issues.

    pasteHandler doesn't work correctly for list items when their content is empty prior to pasting text.

    1 participant